/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.misc;

import javax.media.opengl.GL;

/**
 * Allows easy rendering to a texture.
 */
public class TextureRenderer {
	/**
	 * Checks for errors related to frame buffer objects.
	 */
	public static void checkFramebufferStatus(GL gl) {
		int status;
		status = gl.glCheckFramebufferStatusEXT(GL.GL_FRAMEBUFFER_EXT);
		switch (status) {
			case GL.GL_FRAMEBUFFER_COMPLETE_EXT:
				return;
				
			case GL.GL_FRAMEBUFFER_UNSUPPORTED_EXT:
				System.out.println("Unsupported framebuffer format.");
				break;
			
			case GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
				System.out.println("Framebuffer incomplete, missing attachment.");
				break;
			
			case GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
				System.out.println("Framebuffer incomplete, attached images must have same dimensions.");
				break;
			
			case GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
				System.out.println("Framebuffer incomplete, attached images must have same format.");
				break;
			
			case GL.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
				System.out.println("Framebuffer incomplete, missing draw buffer.");
				break;
			
			case GL.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
				System.out.println("Framebuffer incomplete, missing read buffer.");
				break;
			
			default:
				System.out.println("Error " + status);
				break;
		}
		
		// frame buffer not complete
		System.exit(1);
	}
	
	/**
	 * Sets texture parameters used for textures as render targets.
	 */
	public static void setTextureParameters(GL gl) {
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
	}
	
	private int[] targetFramebuffer = { 0 };
	
	private int[] targetDSBuffer    = { 0 };
	private int[] targetTexture     = { 0 };
	
	private int   buffWidth         = 0;
	
	private int   buffHeight        = 0;
	
	/**
	 * Constructs a TextureRenderer.
	 */
	public TextureRenderer(GL gl) {
	}
	
	/**
	 * Binds the texture of this render target to the current texture unit.
	 */
	public void bindTexutre(GL gl) {
		gl.glBindTexture(GL.GL_TEXTURE_2D, targetTexture[0]);
	}
	
	/**
	 * Disables the frame buffer object by binding the standard frame buffer.
	 */
	public void disable(GL gl) {
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
		gl.glPopAttrib();
	}
	
	/**
	 * Enables the frame buffer object by binding it.
	 */
	public void enable(GL gl) {
		gl.glPushAttrib(GL.GL_VIEWPORT_BIT);
		gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, targetFramebuffer[0]);
		gl.glViewport(0, 0, buffWidth, buffHeight);
	}
	
	/**
	 * Initializes the frame buffer object and texture associated with this object.
	 */
	public void init(GL gl, int width, int height) {
		buffWidth = width;
		buffHeight = height;
		
		// generate and bind frame buffer
		gl.glGenFramebuffersEXT(1, targetFramebuffer, 0);
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, targetFramebuffer[0]);
		
		// generate and bind texture
		gl.glGenTextures(1, targetTexture, 0);
		gl.glBindTexture(GL.GL_TEXTURE_2D, targetTexture[0]);
		setTextureParameters(gl);
		gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, width, height, 0, GL.GL_RGBA, GL.GL_FLOAT, null);
		gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_COLOR_ATTACHMENT0_EXT, GL.GL_TEXTURE_2D,
		        targetTexture[0], 0);
		checkFramebufferStatus(gl);
		
		// generate and bind depth/stencil buffer
		gl.glGenRenderbuffersEXT(1, targetDSBuffer, 0);
		gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, targetDSBuffer[0]);
		gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH24_STENCIL8_EXT, width, height);
		gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT,
		        targetDSBuffer[0]);
		gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_STENCIL_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT,
		        targetDSBuffer[0]);
		checkFramebufferStatus(gl);
		
		// disable frame buffer object
		gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	}
	
	/**
	 * Renders the texture associated with this object onto the whole screen.
	 */
	public void renderOnscreen(GL gl) {
		// configure matrices
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		gl.glOrtho(-1., 1., -1., 1., -1., 1.);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		// draw texture onto the whole screen
		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2d(0., 0.);
		gl.glVertex2d(-1., -1.);
		gl.glTexCoord2d(1., 0.);
		gl.glVertex2d(1., -1.);
		gl.glTexCoord2d(1., 1.);
		gl.glVertex2d(1., 1.);
		gl.glTexCoord2d(0., 1.);
		gl.glVertex2d(-1., 1.);
		gl.glEnd();
		
		// restore projection matrix and reset matrix mode
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPopMatrix();
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPopMatrix();
	}
	
	/**
	 * Resizes the texture associated with this object.
	 */
	public void resize(GL gl, int width, int height) {
		buffWidth = width;
		buffHeight = height;
		
		// resize texture
		gl.glBindTexture(GL.GL_TEXTURE_2D, targetTexture[0]);
		gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, width, height, 0, GL.GL_RGBA, GL.GL_FLOAT, null);
		
		// resize depth/stencil buffer
		gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, targetDSBuffer[0]);
		gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH24_STENCIL8_EXT, width, height);
	}
}
